package com.hcw.bankdemo.service.impl;

import com.hcw.bankdemo.mapper.TradeLogMapper;
import com.hcw.bankdemo.model.Account;
import com.hcw.bankdemo.model.TradeLog;
import com.hcw.bankdemo.service.AccountService;
import com.hcw.bankdemo.exception.ExceptionEnum;
import com.hcw.bankdemo.exception.ServiceException;
import com.hcw.bankdemo.mapper.AccountMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Isolation;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.client.RestTemplate;

import javax.annotation.Resource;
import java.math.BigDecimal;
import java.util.Date;

/**
 * @ClassName AccountServiceImpl
 * @description: 账户实现类
 * @author: hcw
 * @time: 2020/8/29 11:01
 */
@Slf4j
@Service
public class AccountServiceImpl implements AccountService {
    @Resource
    AccountMapper accountMapper;
    @Autowired
    RestTemplate restTemplate;
    @Resource
    TradeLogMapper tradeLogMapper;


    /**
     * @description: 思路：  1.AB 同库放在一个事务中即可
     * 2.多线程情况下为了性能更高，放弃synchronized和串行访问，另外为了防止太多失败不采用乐观锁,直接采用悲观所 select for update
     * @params: fromName toName  money ,
     * @return: 是否成功, 一个系统直接看日志，对接外部系统需返回错误类型。
     * @author: hcw
     * @time: 2020/8/29 15:07
     */
    @Override
    @Transactional(timeout = 5, rollbackFor = Exception.class,propagation = Propagation.REQUIRES_NEW,isolation= Isolation.READ_COMMITTED)
    public boolean transferMoney(String fromName, String toName, BigDecimal money) {
        try {
            //发送方和接受方都需要加行锁，以防处理过程中有别的线程更改数据。
            //接收方账号验证
            Account b = accountMapper.getAccountByNameLock(toName);
            if (b == null || b.getStatus() == 0) {
                log.info(toName + "账户异常");
                return false;
            }
            //发送方账号验证
            Account a = accountMapper.getAccountByNameLock(fromName);
            if (a == null || a.getStatus() == 0) {
                log.info(fromName + "账户异常");
                return false;
            }
            BigDecimal currentMoneyA = a.getCurrentMoney();
            //账户余额不够
            if (currentMoneyA.compareTo(money) == -1) {
                log.info("账户余额不够");
                return false;
            }
            //能转款，先把a的账户扣款
            currentMoneyA = currentMoneyA.subtract(money);
            int statusA = accountMapper.updateAccountByAccountName(fromName, currentMoneyA);
            if (statusA == 0) {
                log.info("转账账户更新异常，请检查sql日志");
                return false;
            }
            //给B账户加钱
            BigDecimal currentMoneyB = b.getCurrentMoney();
            currentMoneyB = currentMoneyB.add(money);
            int statusB = accountMapper.updateAccountByAccountName(toName, currentMoneyB);
            //异常测试
//            int[] arr=  new int[1];
//            arr[2] =3;
            if (statusB == 0) {
                throw new ServiceException("收款B更新失败，请查看日志", ExceptionEnum.SERVER_TRANT_ERROR,null);
            }
            return true;
        } catch (Exception e) {
            throw new ServiceException(e.getMessage(), ExceptionEnum.SERVER_TRANT_ERROR,e);
        }
    }


    /**
     * @param fromName,toName,money
     * @description: 转款方的服务，
     * @return: 为了方便 另外A和B服务返回参数较简单，没有设计成包含错误码和具体错误类型的对象
     * @author: hcw
     * @time: 2020/8/29 16:20
     */
    @Override
    @Transactional(propagation = Propagation.REQUIRES_NEW,timeout =10, rollbackFor = Exception.class,isolation= Isolation.READ_COMMITTED)
    public boolean fromAccoutTransfer(String fromName, String toName, BigDecimal money) {
        try {
            //转款方A账号验证
            Account a = accountMapper.getAccountByNameLock(fromName);
            if (a == null || a.getStatus() == 0) {
                log.info(fromName + "账户异常");
                return false;
            }
            BigDecimal currentMoneyA = a.getCurrentMoney();
            //账户余额不够
            if (currentMoneyA.compareTo(money) == -1) {
                log.info("账户余额不够");
                return false;
            }
            //能转款，先把a的账户扣款
            currentMoneyA = currentMoneyA.subtract(money);
            int statusA = accountMapper.updateAccountByAccountName(fromName, currentMoneyA);
            if (statusA == 0) {
                log.info("转账账户更新异常，请检查sql日志");
                return  false;
            }
            try {
                log.info("开始转账给B用户,记录一条转账数据DATA_A1在数据库里面,作为以后的凭证,和逆向订单查询以防重复执行");
                TradeLog tradeLog = TradeLog.builder().dateTime(new Date()).fromName(fromName).money(money).status(0).toName(toName).build();
                tradeLogMapper.insertTradeLog(tradeLog);
                int aId = tradeLog.getId();
                //这里模拟 调用收款方服务B库的服务，为了简便没有引入ribbon等，采用直接访问应用的地址
              //  Boolean result = restTemplate.getForObject("http://127.0.0.1:8002/outTradeServiceB/transferMoney?fromName="+fromName+"&toName="+toName+"&money="+money.toString()+"&aId="+aId,Boolean.class);
                Boolean result = restTemplate.getForObject("http://serviceB-provider/outTradeServiceB/transferMoney?fromName="+fromName+"&toName="+toName+"&money="+money.toString()+"&aId="+aId,Boolean.class);
                if (!result) {
                    //如果收款方B服务返回失败，那么需要把转款方A的扣款回滚
                    log.info("转账给B用户失败,修改记录在数据库里面的转账记录DATA_A1,记录失败原因");
                    tradeLogMapper.updateTradeLogById(aId,2,"收款方内部数据出错回滚A事务");
                    throw new ServiceException("收款方内部数据出错,回滚A事务",ExceptionEnum.SERVER_OTHER_ERROR,null);
                }
                /**
                 * 1. 同步调用的情况下直接修改记录在数据库里面的转账记录,标记成功
                 * 2.异步调用的情况下，需要提供一个服务给调用方，让调用方执行成功后更新这个转账记录。
                 */
                log.info("转账给B用户成功,修改记录在数据库里面的转账记录DATA_A1,标记成功");
                tradeLogMapper.updateTradeLogById(tradeLog.getId(),1,"");
                return  true;
            } catch (Exception e) {
                /**
                 * 调用B服务过程中出问题
                 * 基本逻辑
                 * 1.如果调用收款方接口超时或者网络等问题，是回滚此事务，不给A的钱
                 * 2.记录一下这次调用失败操作，最好是存在数据库里面，然后定时处理或者手动处理一下。
                 * 这里的失败涉及两个问题 1.收款方B服务是执行成功了，但是传输过程出问题了。这个需要调用逆向回滚
                 *                    2.收款方的接口根本没有执行，不需要调用逆向回滚。
                 *
                 *处理逻辑:  在收款方B那边dBbAccountServiceImpl.toAccoutTransfer做一个处理的记录。 然后这需要查询一次，再处理，防止重复
                 * 1、如果是已经处理过了就调用  dBbAccountServiceImpl.noToAccoutTransfer()回滚B库的数据。
                 * 2、如果B那边没有处理过，那么这边直接回滚A库的操作即可。
                 *
                 * //因为没有写日志到数据库 我这就只处理第2个逻辑。
                 */
                throw new ServiceException(fromName+",调用收款方接口dBbAccountServiceImpl.toAccoutTransfer出错", ExceptionEnum.SERVER_To_RPC_ERROR,e);
            }
        } catch (Exception e) {
            //这里的异常只能处理本事务的异常，不会处理调用收款方 B服务的 异常，所以不需要逆向收款方
            throw new ServiceException(e.getMessage(), ExceptionEnum.SERVER_TRANT_ERROR,e);
        }
    }

    /**
     * @param
     * @description: 转款方的逆向服务, 转款的逆向服务（转款成功后 ，收款失败务失败了 回滚收款方数据）
     * @return: 为了方便 另外A和B服务返回参数较简单，没有设计成包含错误码和具体错误类型的对象
     * @author: hcw
     * @time: 2020/8/29 16:39
     */
    @Override
    @Transactional(propagation = Propagation.REQUIRES_NEW,timeout = 10, rollbackFor = Exception.class)
    public boolean noFromAccoutTransfer(String fromName,String toName, BigDecimal money,int aId) {
        try {
            /**
             * 执行逻辑
             * 1.首先要查询转账记录DATA_A1,防止重复
             * 2.验证通过后，再更新数据库
             */
            log.info("查询转账记录DATA_A1,防止重复冲红");
            TradeLog tradeLog =   tradeLogMapper.getTradeLogById(aId);

            if (tradeLog == null ) {
                log.info("没找到转账记录");
                return false;
            }
            //已经逆向了不操作。
            if (tradeLog.getStatus() == 1) {
                log.info("已经执行过逆向冲红，不需要再操作");
                return false;
            }
            //转款方A账号验证
            Account a = accountMapper.getAccountByNameLock(fromName);
            if (a == null || a.getStatus() == 0) {
                log.info(fromName + "账户异常");
                return false;
            }
            //把账号的钱冲回去。
            BigDecimal currentMoneyA = a.getCurrentMoney();
            currentMoneyA = currentMoneyA.add(money);
            int statusA = accountMapper.updateAccountByAccountName(fromName, currentMoneyA);
            if (statusA == 0) {
                log.info("转账账户更新异常，请检查sql日志");
                return  false;
            }
            //这个日志记录很重要，涉及到调用方重复逆向回滚问题 。最好存在数据库。DBbAccountServiceImpl类 中的65行左右有涉及到这个问题
            log.info(fromName+"转账给"+toName+",对应的逆向回滚接口调用成功。");
            return true;
        } catch (Exception e) {
            throw new ServiceException(e.getMessage(), ExceptionEnum.SERVER_OTHER_ERROR,e);
        }
    }
}